Skip to content

fix: resolve submodules from the current checkout's .gitmodules - #2767

Open
TheGreatApollyon wants to merge 2 commits into
copier-org:masterfrom
TheGreatApollyon:fix/submodule-urls-from-gitmodules
Open

fix: resolve submodules from the current checkout's .gitmodules#2767
TheGreatApollyon wants to merge 2 commits into
copier-org:masterfrom
TheGreatApollyon:fix/submodule-urls-from-gitmodules

Conversation

@TheGreatApollyon

Copy link
Copy Markdown

Summary

When a template's submodule moves to a new repository between versions, copier update fails while checking out the newer version:

fatal: remote error: upload-pack: not our ref <commit>
fatal: Fetched in submodule path '...', but it did not contain <commit>. Direct fetching of that commit failed.

Root cause

Worktrees created from the cached mirror share the mirror's config. git submodule update --init in an earlier checkout registers submodule.<name>.url entries there, pointing at that checkout's submodule URLs. Those registrations silently override the .gitmodules of later checkouts (the config takes precedence over .gitmodules), so the newer checkout tries to fetch its pinned commit from the old, stale location and fails.

Fix

Before running git submodule update on a worktree, drop any submodule.<name>.url registrations from the mirror's config. Every checkout then resolves its submodules from its own .gitmodules.

This also fixes updates for users who already have a polluted mirror cache, since the cleanup runs on every checkout.

Test

Added a regression test that clones a template whose submodule moved (URL changed between two tags), asserting that both revisions check out with the correct submodule content. Verified it fails on the previous code with the exact error above.

Fixes #2766

When a template moves one of its submodules to a new repository,
`copier update` fails on the newer checkout with `fatal: remote error:
upload-pack: not our ref <commit>`.

Worktrees created from the cached mirror share the mirror's config, so
`git submodule update --init` in an earlier checkout registers
`submodule.<name>.url` entries pointing at that checkout's submodule
URLs. Those registrations override the .gitmodules of later checkouts,
which then try to fetch the pinned commit from the stale URL.

Drop any `submodule.<name>.url` registrations from the mirror's config
before updating submodules, so every checkout resolves them from its
own .gitmodules.
Copilot AI review requested due to automatic review settings July 31, 2026 03:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a copier update failure mode when a template submodule’s URL changes between revisions by ensuring each cached worktree resolves submodules from its own .gitmodules rather than stale submodule.<name>.url entries persisted in the shared mirror config.

Changes:

  • In the cached-mirror worktree flow, removes any submodule.<name>.url entries from the mirror’s config before running git submodule update.
  • Adds a regression test that creates a template whose submodule URL changes between two tags and verifies both revisions check out the correct submodule content.
  • Adds test environment configuration to allow local (file:///path) submodules under modern Git defaults.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
copier/_vcs.py Cleans stale submodule.*.url entries from the shared mirror config before submodule checkout in cached worktrees.
tests/test_vcs.py Adds a regression test for submodule URL moves across revisions, plus Git env setup to allow local submodules in tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_vcs.py Outdated
Comment on lines +208 to +218
@pytest.fixture(scope="session", autouse=True)
def allow_file_submodules() -> None:
"""Allow the fixture repos below to be used as submodules.

Since Git 2.38.1 the file protocol is blocked for submodules by default
(see GHSA-3wp6-j8xr-qw85), so local submodules require this setting. Set
it via the environment so it also applies to Copier's own subprocesses.
"""
local.env["GIT_CONFIG_COUNT"] = "1"
local.env["GIT_CONFIG_KEY_0"] = "protocol.file.allow"
local.env["GIT_CONFIG_VALUE_0"] = "always"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed to scope="module" with with local.env(...): yield so the environment variables are automatically restored after each test module. Good catch, thanks!

@sisp sisp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for discovering this bug and submitting a PR that fixes it, @TheGreatApollyon! 🙇

I've left two inline suggestions.

Comment thread tests/test_vcs.py
Comment on lines +208 to +221
@pytest.fixture(scope="module", autouse=True)
def allow_file_submodules() -> None:
"""Allow the fixture repos below to be used as submodules.

Since Git 2.38.1 the file protocol is blocked for submodules by default
(see GHSA-3wp6-j8xr-qw85), so local submodules require this setting. Set
it via the environment so it also applies to Copier's own subprocesses.
"""
with local.env(
GIT_CONFIG_COUNT="1",
GIT_CONFIG_KEY_0="protocol.file.allow",
GIT_CONFIG_VALUE_0="always",
):
yield

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about setting

git = git["-c", "protocol.file.allow=always"]

at the beginning of test_remote_clone_submodule_with_moved_url instead of using this autouse fixture? This would avoid enabling the file protocol for the entire module and setting Git config environment variables (which would need incrementing GIT_CONFIG_COUNT instead of overwriting it and using the new index for GIT_CONFIG_{KEY,VALUE}_<N> even)?

Comment thread copier/_vcs.py
Comment on lines +340 to +361
# Worktrees share the mirror's config, so `git submodule update
# --init` from an earlier checkout may have registered
# `submodule.<name>.url` entries pointing at that checkout's
# submodule URLs, silently overriding the current `.gitmodules`
# (e.g. after a submodule moved to a new repository). Drop any
# registrations so each checkout resolves its submodules from its
# own `.gitmodules`.
for key in git(
"config",
"--file",
str(mirror / "config"),
"--get-regexp",
r"^submodule\..+\.url$",
retcode=None,
).splitlines():
git(
"config",
"--file",
str(mirror / "config"),
"--unset-all",
key.split()[0],
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify and robustify this using git submodule sync:

Suggested change
# Worktrees share the mirror's config, so `git submodule update
# --init` from an earlier checkout may have registered
# `submodule.<name>.url` entries pointing at that checkout's
# submodule URLs, silently overriding the current `.gitmodules`
# (e.g. after a submodule moved to a new repository). Drop any
# registrations so each checkout resolves its submodules from its
# own `.gitmodules`.
for key in git(
"config",
"--file",
str(mirror / "config"),
"--get-regexp",
r"^submodule\..+\.url$",
retcode=None,
).splitlines():
git(
"config",
"--file",
str(mirror / "config"),
"--unset-all",
key.split()[0],
)
git("submodule", "sync", "--recursive")

WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression since v9.16.0: git template caching

3 participants